在討論如何使用 Immutable 的方式來操作物件的狀態前,讓我們來思考另外一個狀況:還記得前端常常考的面試題淺拷貝、深拷貝嗎?
在這邊我們不會以深拷貝、淺拷貝的角度切入討論更新物件的方式,原因在於其實在實務開發中鮮少要完全拷貝某個物件的結構,單純為了獲得兩個記憶體不同,但資料結構一模一樣的物件,更多的是在不改動原始資料的狀況下,進行資料的處理。
因此,雖然不太會有真的要完整複製某個資料結構的狀況發生,但絕對會有很常需要「更新」物件的資料的時候,那要怎麼透過 Immutable 的方式,在資料彼此獨立、互不影響的狀況下來更新物件的內容呢?
在這個章節我們要來討論如何使用 ES6 以後的語法糖:展開運算子(Spread Operator)與其餘參數(Rest Parameter)來做到物件的更新。
在 ES6 後新增了一個運算子,可以幫助我們去展開物件、陣列原有的屬性或是元素,這是什麼意思呢?我們來看以下範例:
const box1 = {
apple: 1,
banana: 1,
};
const box2 = { ...box1 };
// box2 = {apple: 1, banana: 1}
展開運算子會將後方帶入的物件內容展開並去掉大括號,所以我們要再補一組大括號回去,此時我們就會獲得一組不僅資料結構相同,參考物件也不同的新物件。
但要注意的是,透過展開運算子展開的物件只能做到淺拷貝,若有兩層以上的階層,依然會有物件屬性值傳參考的狀況出現,為了避免這個狀況,盡量只進行單層物件結構的展開。
於是現在比起透過這樣的方式進行拷貝(實際上下方的範例並沒有進行拷貝的行為):
const box1 = {
apple: 1,
banana: 1,
};
const box2 = box1;
// box2 = {apple: 1, banana: 1}
我們可以透過展開運算子透過 Immutable 的方式複製物件了!
那我們要怎麼透過這個特性進行物件的更新呢?結合函式的概念我們現在可以這樣更新物件的值:
const box1 = {
apple: 1,
banana: 1,
};
const updateAppleInBox1 = () => ({...box1, apple: box1.apple+1});
console.log(updateAppleInBox1());
// output {apple: 2, banana: 1}
透過這樣的方式更新物件會發現,我們不僅得到了更新後的物件,且原始的物件也沒有被更改到耶!
那如果要刪除物件屬性怎麼辦呢?我們可以透過物件解構(Object Destructuring)搭配剩餘參數做到這件事:
其餘參數的樣子其實跟展開運算子長得一樣,只是會依據我們使用的場合而決定,要展開的是「所有的內容」或是被解構剩餘的剩下屬性值,搭配上方的概念,我們可以這樣使用其餘參數:
const box1 = {
apple: 1,
banana: 1,
};
const deleteApplePropInBox1 = () => {
const { apple, ...rest} = box1;
return {...rest};
};
console.log(deleteApplePropInBox1());
// output {banana: 1}
範例中,我們將不想要的屬性獨立出來,並且將剩下我們要保留的屬性透過其餘參數的運算子語法保留進 rest
變數中,最後透過展開運算子在空物件中展開 rest
並且回傳,這樣我們就可以依照 Immutable 的原則,在不改變原物件的狀況下,取得刪除屬性的新物件了!
但要注意的是,展開運算子、其餘參數盡量以單層物件的概念去操作,不然就會有共用參考位置的問題出現,一不小心就會更新錯資料。
為了避免物件的 Mutation,即便使用了展開運算子、其餘參數,也不要它們當作是萬用丹,要時常注意資料的層級問題。
其實講到這邊不難發現,範例越來越有被模組化的感覺了,但這其實還只是 FP 核心概念的冰山一角,在進入到其他概念前,我們也來看看如何透過依據 Immutable 的原則來進行陣列的更新!那我們下一章節見!
Functional Programming For Beginners With JavaScript